import React from "react";
import { act, waitFor } from "@testing-library/react";

import {
    MockJSONServer,
    mockLegacyRouterProvider,
    render,
    TestWrapper,
} from "@test";

import { Authenticated } from "./";
import { AuthBindings } from "src/interfaces";

const legacyMockAuthProvider = {
    login: () => Promise.resolve(),
    logout: () => Promise.resolve(),
    checkError: () => Promise.resolve(),
    checkAuth: () => Promise.resolve(),
    getPermissions: () => Promise.resolve(["admin"]),
    getUserIdentity: () => Promise.resolve(),
};

const mockReplace = jest.fn();

const mockLegacyRouter = {
    ...mockLegacyRouterProvider(),
    useHistory: () => ({
        replace: mockReplace,
    }),
    useLocation: () => ({
        pathname: "/posts",
        search: "",
    }),
};

const mockAuthProvider: AuthBindings = {
    login: () =>
        Promise.resolve({
            success: true,
        }),
    logout: () => Promise.resolve({ success: true }),
    onError: () => Promise.resolve({}),
    check: () => Promise.resolve({ authenticated: true }),
    getPermissions: () => Promise.resolve(),
};

describe("v3LegacyAuthProviderCompatible Authenticated", () => {
    beforeEach(() => {
        jest.spyOn(console, "error").mockImplementation((message) => {
            if (typeof message !== "undefined") console.warn(message);
        });
    });

    it("should render children successfully", async () => {
        const { getByText } = render(
            <Authenticated
                key="should-render-children-legacy"
                v3LegacyAuthProviderCompatible={true}
            >
                Custom Authenticated
            </Authenticated>,
            {
                wrapper: TestWrapper({
                    dataProvider: MockJSONServer,
                    legacyAuthProvider: legacyMockAuthProvider,
                    resources: [{ name: "posts", route: "posts" }],
                }),
            },
        );

        await waitFor(() => getByText("Custom Authenticated"));
    });

    it("not authenticated test", async () => {
        const { queryByText } = render(
            <Authenticated
                key="not-authenticated-legacy"
                v3LegacyAuthProviderCompatible={true}
            >
                Custom Authenticated
            </Authenticated>,
            {
                wrapper: TestWrapper({
                    dataProvider: MockJSONServer,
                    legacyAuthProvider: {
                        ...legacyMockAuthProvider,
                        checkAuth: () => Promise.reject(),
                    },
                    legacyRouterProvider: mockLegacyRouter,
                    resources: [{ name: "posts", route: "posts" }],
                }),
            },
        );

        await waitFor(() => {
            expect(queryByText("Custom Authenticated")).toBeNull();
            expect(mockReplace).toBeCalledTimes(1);
        });
    });

    it("not authenticated fallback component test", async () => {
        legacyMockAuthProvider.checkAuth = jest
            .fn()
            .mockImplementation(() => Promise.reject());

        const { queryByText } = render(
            <Authenticated
                key="fallback-component-test"
                fallback={<div>Error fallback</div>}
                v3LegacyAuthProviderCompatible={true}
            >
                Custom Authenticated
            </Authenticated>,
            {
                wrapper: TestWrapper({
                    dataProvider: MockJSONServer,
                    legacyAuthProvider: legacyMockAuthProvider,
                    resources: [{ name: "posts", route: "posts" }],
                }),
            },
        );

        await act(async () => {
            expect(queryByText("Error fallback"));
        });
    });

    it("loading test", async () => {
        legacyMockAuthProvider.checkAuth = jest
            .fn()
            .mockImplementation(() => Promise.reject());

        const { queryByText } = render(
            <Authenticated
                key="loading-test"
                loading={<div>loading</div>}
                v3LegacyAuthProviderCompatible={true}
            >
                Custom Authenticated
            </Authenticated>,
            {
                wrapper: TestWrapper({
                    dataProvider: MockJSONServer,
                    legacyAuthProvider: legacyMockAuthProvider,
                    resources: [{ name: "posts", route: "posts" }],
                }),
            },
        );

        await act(async () => {
            expect(queryByText("loading"));
        });
    });
});

describe("Authenticated", () => {
    beforeEach(() => {
        jest.spyOn(console, "error").mockImplementation((message) => {
            if (typeof message !== "undefined") console.warn(message);
        });
    });

    it("should render children successfully", async () => {
        const { getByText } = render(
            <Authenticated key="render-children-successfully">
                Custom Authenticated
            </Authenticated>,
            {
                wrapper: TestWrapper({
                    dataProvider: MockJSONServer,
                    authProvider: mockAuthProvider,
                    resources: [{ name: "posts", route: "posts" }],
                }),
            },
        );

        await waitFor(() => getByText("Custom Authenticated"));
    });

    it("not authenticated test", async () => {
        const { queryByText } = render(
            <Authenticated key="not-authenticated-test">
                Custom Authenticated
            </Authenticated>,
            {
                wrapper: TestWrapper({
                    dataProvider: MockJSONServer,
                    authProvider: {
                        ...mockAuthProvider,
                        check: () => Promise.resolve({ authenticated: false }),
                    },
                    resources: [{ name: "posts", route: "posts" }],
                    legacyRouterProvider: mockLegacyRouter,
                }),
            },
        );

        await waitFor(() => {
            expect(queryByText("Custom Authenticated")).toBeNull();
            expect(mockReplace).toBeCalledTimes(1);
        });
    });

    it("not authenticated fallback component test", async () => {
        mockAuthProvider.check = jest.fn().mockImplementation(() =>
            Promise.resolve({
                authenticated: false,
                error: new Error("Not authenticated"),
            }),
        );

        const { queryByText } = render(
            <Authenticated
                key="not-authenticated-fallback-component-test"
                fallback={<div>Error fallback</div>}
            >
                Custom Authenticated
            </Authenticated>,
            {
                wrapper: TestWrapper({
                    dataProvider: MockJSONServer,
                    authProvider: mockAuthProvider,
                    resources: [{ name: "posts", route: "posts" }],
                }),
            },
        );

        await act(async () => {
            expect(queryByText("Error fallback"));
        });
    });

    it("loading test", async () => {
        const { queryByText } = render(
            <Authenticated key="loading-test" loading={<div>loading</div>}>
                Custom Authenticated
            </Authenticated>,
            {
                wrapper: TestWrapper({
                    dataProvider: MockJSONServer,
                    authProvider: mockAuthProvider,
                    resources: [{ name: "posts", route: "posts" }],
                }),
            },
        );

        await act(async () => {
            expect(queryByText("loading"));
        });
    });

    it("should redirect to `/my-path` if not authenticated (authProvider's check)", async () => {
        const mockGo = jest.fn();

        const { queryByText } = render(
            <Authenticated key="should-redirect-custom-provider-check">
                Custom Authenticated
            </Authenticated>,
            {
                wrapper: TestWrapper({
                    dataProvider: MockJSONServer,
                    authProvider: {
                        ...mockAuthProvider,
                        check: async () => {
                            return {
                                authenticated: false,
                                redirectTo: "/my-path",
                            };
                        },
                    },
                    routerProvider: {
                        go: () => mockGo,
                    },
                    resources: [{ name: "posts", route: "posts" }],
                }),
            },
        );

        await act(async () => {
            expect(queryByText("Custom Authenticated")).toBeNull();
        });

        await waitFor(() =>
            expect(mockGo).toBeCalledWith(
                expect.objectContaining({ to: "/my-path", type: "replace" }),
            ),
        );
    });

    it("should redirect to `/my-path` if not authenticated (`redirectOnFail` prop)", async () => {
        const mockGo = jest.fn();

        const { queryByText } = render(
            <Authenticated
                key="should-redirect-custom-on-fail"
                redirectOnFail="/my-path"
            >
                Custom Authenticated
            </Authenticated>,
            {
                wrapper: TestWrapper({
                    dataProvider: MockJSONServer,
                    authProvider: {
                        ...mockAuthProvider,
                        check: async () => {
                            return {
                                authenticated: false,
                                redirectTo: "/other-path",
                            };
                        },
                    },
                    routerProvider: {
                        go: () => mockGo,
                    },
                    resources: [{ name: "posts", route: "posts" }],
                }),
            },
        );

        await act(async () => {
            expect(queryByText("Custom Authenticated")).toBeNull();
        });

        await waitFor(() =>
            expect(mockGo).toBeCalledWith(
                expect.objectContaining({ to: "/my-path", type: "replace" }),
            ),
        );
    });

    it("should redirect to `/my-path` if not authenticated (navigate in fallback)", async () => {
        const mockGo = jest.fn();

        const NavigateComp = ({ to }: { to: string }) => {
            React.useEffect(() => {
                mockGo({ to, type: "replace" });
            }, [to]);

            return null;
        };

        const { queryByText } = render(
            <Authenticated
                key="should-redirect-fallback"
                fallback={<NavigateComp to="/my-path" />}
            >
                Custom Authenticated
            </Authenticated>,
            {
                wrapper: TestWrapper({
                    dataProvider: MockJSONServer,
                    authProvider: {
                        ...mockAuthProvider,
                        check: async () => {
                            return {
                                authenticated: false,
                                redirectTo: "/other-path",
                            };
                        },
                    },
                    routerProvider: {
                        go: () => mockGo,
                    },
                    resources: [{ name: "posts", route: "posts" }],
                }),
            },
        );

        await act(async () => {
            expect(queryByText("Custom Authenticated")).toBeNull();
        });

        await waitFor(() =>
            expect(mockGo).toBeCalledWith(
                expect.objectContaining({ to: "/my-path", type: "replace" }),
            ),
        );
    });

    it("should redirect to `/my-path?to=/dashboard?current=1&pageSize=2` if not authenticated (`redirectOnFail` with append query)", async () => {
        const mockGo = jest.fn();

        const currentQuery = {
            current: 1,
            pageSize: 2,
        };

        const currentPathname = "dashboard";

        const { queryByText } = render(
            <Authenticated
                key="should-redirect-with-to"
                redirectOnFail="/my-path"
                appendCurrentPathToQuery
            >
                Custom Authenticated
            </Authenticated>,
            {
                wrapper: TestWrapper({
                    dataProvider: MockJSONServer,
                    authProvider: {
                        ...mockAuthProvider,
                        check: async () => {
                            return {
                                authenticated: false,
                                redirectTo: "/other-path",
                            };
                        },
                    },
                    routerProvider: {
                        go: () => {
                            return (config) => {
                                if (config.type === "path") {
                                    const params = new URLSearchParams(
                                        currentQuery as any,
                                    );
                                    return `/${config.to}?${params.toString()}`;
                                } else {
                                    return mockGo(config);
                                }
                            };
                        },
                        parse: () => {
                            return () => ({
                                pathname: currentPathname,
                                params: currentQuery,
                            });
                        },
                    },
                    resources: [{ name: "posts", route: "posts" }],
                }),
            },
        );

        await act(async () => {
            expect(queryByText("Custom Authenticated")).toBeNull();
        });

        await waitFor(() =>
            expect(mockGo).toBeCalledWith(
                expect.objectContaining({
                    to: "/my-path",
                    query: {
                        to: "/dashboard?current=1&pageSize=2",
                    },
                    type: "replace",
                }),
            ),
        );
    });

    it("should redirect to `/my-path?to=/dashboard?current=1&pageSize=2` if not authenticated (authProvider's check with append query)", async () => {
        const mockGo = jest.fn();

        const currentQuery = {
            current: 1,
            pageSize: 2,
        };

        const currentPathname = "dashboard";

        const { queryByText } = render(
            <Authenticated
                key="should-redirect-path-from-provider"
                appendCurrentPathToQuery
            >
                Custom Authenticated
            </Authenticated>,
            {
                wrapper: TestWrapper({
                    dataProvider: MockJSONServer,
                    authProvider: {
                        ...mockAuthProvider,
                        check: async () => {
                            return {
                                authenticated: false,
                                redirectTo: "/my-path",
                            };
                        },
                    },
                    routerProvider: {
                        go: () => {
                            return (config) => {
                                if (config.type === "path") {
                                    const params = new URLSearchParams(
                                        currentQuery as any,
                                    );
                                    return `/${config.to}?${params.toString()}`;
                                } else {
                                    return mockGo(config);
                                }
                            };
                        },
                        parse: () => {
                            return () => ({
                                pathname: currentPathname,
                                params: currentQuery,
                            });
                        },
                    },
                    resources: [{ name: "posts", route: "posts" }],
                }),
            },
        );

        await act(async () => {
            expect(queryByText("Custom Authenticated")).toBeNull();
        });

        await waitFor(() =>
            expect(mockGo).toBeCalledWith(
                expect.objectContaining({
                    to: "/my-path",
                    query: {
                        to: "/dashboard?current=1&pageSize=2",
                    },
                    type: "replace",
                }),
            ),
        );
    });
});
